﻿// Copyright 2010, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef SCRW_CURL_H_
#define SCRW_CURL_H_

#include <cctype>
#include <cstring>
#include <deque>
#include <stdexcept>
#include <string>
#include <vector>
#include <utility>

#include <libv/cast.h>
#include <libv/check.h>
#include <libv/cistring.h>
#include <libv/lock.h>
#include <libv/uncopyable.h>

#include <curl/curl.h>

#include "http_header_parser.h"
#include "openssl_init.h"
#include "common.h"


namespace scrw
{

/** Базовое исключение, генерируемое классом curl,
  * при возникновении ошибок внутри библиотеки libcurl. */
class CurlException : public ::std::exception
{
  public:
    explicit CurlException(const ::std::string& msg) : msg_(msg) {}
    virtual ~CurlException() throw() {}

    virtual const char* what() const throw() { return msg_.c_str(); }

  private:
    ::std::string msg_;
};


/** Исключение, генерируемое методом Curl::Handle::Perform при прерывании
  * загрузки документа, имеющего неподходящий mime-тип. */
class FileSkippedException : public CurlException
{
  public:
    explicit FileSkippedException()
       : CurlException("FileTypeFilter returned"
                       "FileTypeFilter::BREAK_DOWNLOAD") {}
    virtual ~FileSkippedException() throw() {}
};





/** RAII-объект, инициализирующий Curl в конструкторе и очищающий в деструкторе.
  * Так как создавать объект нужно один раз, рекомендуется использование
  * класса-одиночки (синглтона) TheCurlInitializationAndCleanUp, в таком
  * случае первый вызов TheCurlInitializationAndCleanUp::Instance() проведет
  * инициализацию, а последующие будут проигнорированы. */
class CurlInitializationAndCleanUp : libv::Uncopyable
{
  public:
    CurlInitializationAndCleanUp()
    {
        // Инициализация OpenSSL, которая будет произведена 1 раз.
        TheOpenSSLInitializationAndCleanUp::Instance();

        VERIFY(!curl_global_init(CURL_GLOBAL_ALL));
    }

    ~CurlInitializationAndCleanUp()
    {
        curl_global_cleanup();
    }
};

typedef Loki::SingletonHolder<
    CurlInitializationAndCleanUp,
    Loki::CreateUsingNew,
    Loki::DefaultLifetime,
    Loki::ClassLevelLockable> TheCurlInitializationAndCleanUp;





/** Потокобезопасная ООП-обертка над библиотекой curl.
  * Основная операция: создание непотокобезопасного хэндла Curl::Handle.
  * Хэндлы позволяют настраивать свое поведение.
  * Для упрощения работы с хэндлами, класс Curl предоставляем методы,
  * позволяющие общие настройки для всех создаваемых хэндлов.
  *
  * Пример работы с классом:
  * @code
  * Curl curl;
  * curl.set_default_user_agent("Opera/9.80");
  * Curl::Handle curl_handle = curl.GetHandle();  // удалять хэндл ненужно
  * Curl::Handle::PerformResult pr = curl_handle.Perform("http://google.com");
  * ::std::string result_string(&pr[0], pr.size());
  * ::std::cout << "Page MimeType: " << pr.mime_type << "\n"
  *             << pr.data << ::std::endl;
  * @endcode */
class Curl : libv::Uncopyable
{
  public:
    class Handle;

    /** Инициализация внутренних структур данных curl.
      * Функция должна вызываться строго один раз в одном потоке.
      * Таким образом, рекомендуется использовать паттерн «Синглтон».
      * @exception libv::VerifyException. */
    Curl()
        : curlsh_(NULL),
          curlsh_rw_mutex_and_lock_type_(
              ::std::make_pair(new libv::ReadWriteSpin, CURL_LOCK_ACCESS_NONE)),
          default_proxy_is_set_(false),
          default_user_agent_is_set_(false),
          default_referer_is_set_(false),
          default_connect_timeout_is_set_(false),
          default_timeout_is_set_(false),
          default_low_speed_limit_is_set_(false),
          default_cookie_is_set_(false),
          default_max_num_redirects_is_set_(false)
    {
        // Асинхронное использование DNS необходимо в многопоточной среде.
        VERIFY_MSG(using_async_dns(), "Please recompile libcurl with async dns resolver");

        // Инициализация Curl, которая будет произведена 1 раз.
        TheCurlInitializationAndCleanUp::Instance();

        curlsh_ = curl_share_init();
        VERIFY(curlsh_ != NULL);

        VERIFY(!curl_share_setopt(curlsh_, CURLSHOPT_LOCKFUNC, LockFunction));
        VERIFY(!curl_share_setopt(curlsh_,CURLSHOPT_UNLOCKFUNC,UnlockFunction));
        VERIFY(!curl_share_setopt(curlsh_, CURLSHOPT_USERDATA,
                                  &curlsh_rw_mutex_and_lock_type_));

        // Все хэндлы имеют только общий кэш DNS и Cookie.
        VERIFY(!curl_share_setopt(curlsh_, CURLSHOPT_SHARE,
                                  CURL_LOCK_DATA_DNS));
        VERIFY(!curl_share_setopt(curlsh_, CURLSHOPT_SHARE,
                                  CURL_LOCK_DATA_COOKIE));
    }


    /** Очистка внутренних структур данных curl.
      * Функция должна вызываться строго один раз в одном потоке. */
    ~Curl()
    {
        // Игнорируем код ошибки, так как нет способа вернуть ее из деструктора.
        curl_share_cleanup(curlsh_);

        // Блокировки должна удаляться после curlsh_,
        // так как curl_share_cleanup использует ее.
        delete curlsh_rw_mutex_and_lock_type_.first;
        curlsh_rw_mutex_and_lock_type_.first = NULL;
    }


    /** Создание хэндла с заданными ранее настройками и общим кэшем DNS. */
    inline Handle GetHandle() const;

   /** Задание аргумента, передаваемого методу Handle::set_proxy,
      * для всех создаваемых в дальнейшем хэндлов. */
    void set_default_proxy(const ::std::string& proxy)
    {
        libv::Lock dnd(&lock_);
        default_proxy_is_set_ = true;
        default_proxy_ = proxy;
    }


    /** Задание аргумента, передаваемого методу Handle::set_user_agent,
      * для всех создаваемых в дальнейшем хэндлов. */
    void set_default_user_agent(const ::std::string& user_agent)
    {
        libv::Lock dnd(&lock_);
        default_user_agent_is_set_ = true;
        default_user_agent_ = user_agent;
    }


    /** Задание аргумента, передаваемого методу Handle::set_referer,
      * для всех создаваемых в дальнейшем хэндлов. */
    void set_default_referer(const ::std::string& referer)
    {
        libv::Lock dnd(&lock_);
        default_referer_is_set_ = true;
        default_referer_ = referer;
    }


    /** Задание аргумента, передаваемого методу Handle::set_connect_timeout,
      * для всех создаваемых в дальнейшем хэндлов. */
    void set_default_connect_timeout(const size_t timeout)
    {
        libv::Lock dnd(&lock_);
        default_connect_timeout_is_set_ = true;
        default_connect_timeout_ = timeout;
    }


    /** Задание аргумента, передаваемого методу Handle::set_timeout,
      * для всех создаваемых в дальнейшем хэндлов. */
    void set_default_timeout(const size_t timeout)
    {
        libv::Lock dnd(&lock_);
        default_timeout_is_set_ = true;
        default_timeout_ = timeout;
    }



    /** Задание аргументов, передаваемых методу Handle::set_low_speed_limit,
      * для всех созданный в дальнейшем хэндлов. */
    void set_default_low_speed_limit(const size_t speed, const size_t duration)
    {
        libv::Lock dnd(&lock_);
        default_low_speed_limit_is_set_ = true;
        default_low_speed_limit_speed_ = speed;
        default_low_speed_limit_duration_ = duration;
    }


    /** Задание аргумента, передаваемого методу Handle::set_cookie,
      * для всех создаваемых в дальнейшем хэндлов. */
    void set_default_cookie(const ::std::string& cookie)
    {
        libv::Lock dnd(&lock_);
        default_cookie_is_set_ = true;
        default_cookie_ = cookie;
    }

    /** Задание аргумента, передаваемого методу Handle::set_max_num_redirects,
      * для всех создаваемых в дальнейшем хэндлов. */
    void set_default_max_num_redirects(const size_t num_redirects)
    {
        libv::Lock dnd(&lock_);
        default_max_num_redirects_is_set_ = true;
        default_max_num_redirects_ = num_redirects;
    }


  private:
    typedef ::std::pair<libv::ReadWriteSpin*, int> RWMutexAndLockType;


    /** Проверка условия использования асинхронного разрешения имен. */
    static bool using_async_dns()
    {
        return ( curl_version_info(CURLVERSION_NOW)->features &
                 CURL_VERSION_ASYNCHDNS ) != 0;
    }


    /** Функция обратного вызова, используемая curl,
      * для блокировки объекта синхронизации.
      * @param access тип блокировки (чтения или записи)
      * @param prw_mutex_and_lock_type указатель на переданный через опции
      *        объект синхронизации. */
    static void LockFunction(CURL* /*handle*/, curl_lock_data /*data*/,
                             curl_lock_access access,
                             void* prw_mutex_and_lock_type)
    {
        RWMutexAndLockType* r =
            libv::PointerCast<RWMutexAndLockType>(prw_mutex_and_lock_type);

        if ( access == CURL_LOCK_ACCESS_SHARED )
        {
            r->first->ReadLock();
        }
        else
        {
            assert(access == CURL_LOCK_ACCESS_SINGLE);
            r->first->WriteLock();
        }

        r->second = static_cast<int>(access);
    }


    /** Функция обратного вызова, используемая curl
      * для разблокировки объекта синхронизации.
      * @param prw_mutex_and_lock_type указатель на переданный через опции
      *        объект синхронизации. */
    static void UnlockFunction(CURL* /*handle*/, curl_lock_data /*data*/,
                               void* prw_mutex_and_lock_type)
    {
        RWMutexAndLockType* r =
            libv::PointerCast<RWMutexAndLockType>(prw_mutex_and_lock_type);

        if ( r->second == CURL_LOCK_ACCESS_SHARED )
        {
            r->first->ReadUnlock();
        }
        else
        {
            assert(r->second == CURL_LOCK_ACCESS_SINGLE);
            r->first->WriteUnlock();
        }

        r->second = static_cast<int>(CURL_LOCK_ACCESS_NONE);
    }


    /** Блокировка данного класса. */
    mutable libv::Spin lock_;

    /** Разделенный хэндл, используемый для хранения общий cookie и dns resolve,
      * передаваемый всем создаваемым хэндлам. */
    CURLSH* curlsh_;

    /** Блокировка чтения-записи для хэндла curlsh_. */
    RWMutexAndLockType curlsh_rw_mutex_and_lock_type_;

    /** Флаг, определяющий было ли задано значение default_proxy_. */
    bool default_proxy_is_set_;

    /** Значение, предедаваемое всем создаваемым хэндлам в качестве аргумента
      * метода set_default_proxy_. */
    ::std::string default_proxy_;

    /** Флаг, определяющий было ли задано значение default_user_agent_. */
    bool default_user_agent_is_set_;

    /** Значение, передаваемое всем создаваемым хэндлам в качестве аргумента
      * метода set_user_agent. */
    ::std::string default_user_agent_;

    /** Флаг, определяющий было ли задано значение default_referer_. */
    bool default_referer_is_set_;

    /** Значение, передаваемое всем создаваемым хэндлам в качестве аргумента
      * метода set_referer. */
    ::std::string default_referer_;

    /** Флаг, определяющий было ли задано значение
      * default_connect_timeout_. */
    bool default_connect_timeout_is_set_;

    /** Значение, передаваемое всем создаваемым хэндлам в качестве аргумента
      * метода set_connect_timeout. */
    size_t default_connect_timeout_;

    /** Флаг, определяющий было ли задано значение
      * default_timeout_. */
    bool default_timeout_is_set_;

    /** Значение, передаваемое всем создаваемым хэндлам в качестве аргумента
      * метода set_timeout. */
    size_t default_timeout_;

   /** Флаг, определяющий были ли заданы значения
     * default_low_speed_limit_speed_ и default_low_speed_limit_duration_. */
    bool default_low_speed_limit_is_set_;

    /** Значение, передаваемое всем создаваемым хэндлам в качестве первого
      * аргумента метода set_low_speed_limit. */
    size_t default_low_speed_limit_speed_;

    /** Значение, передаваемое всем создаваемым хэндлам в качестве второго
      * аргумента метода set_low_speed_limit. */
    size_t default_low_speed_limit_duration_;

    /** Флаг, определяющий было ли задано значение default_cookie_. */
    bool default_cookie_is_set_;

    /** Значение, передаваемое всем создаваемым хэндлам в качестве аргумента
      * set_cookie. */
    ::std::string default_cookie_;

    /** Флаг, определяющий было ли задано значение default_max_num_redirects_.*/
    bool default_max_num_redirects_is_set_;

    /** Значение, передаваемое всем создаваемым хэндлам в качестве аргумента
      * метода max_num_redirects. */
    size_t default_max_num_redirects_;

};





/** Непотокобезопасный объект, управляющий одной из загрузок.
  * Реализован с использованием счетчика ссылок, таким образом,
  * копия будет ссылаться на тот же ресурс. Данное решение позволяет
  * использовать объект в качестве элемента стандартных контейнеров.
  * Все хэндлы имеют общий кэш dns.
  * Все присланные сервером cookie сохраняются хэндлом.
  * Для их очистки следует использовать метод clear_cookies(). */
class Curl::Handle
{
  friend class Curl;
  public:
    /** Структура, используемая в качестве возвращаемого типа метода Perform. */
    struct PerformResult
    {
        PerformResult() {}

        PerformResult(const ::std::vector<char>& data_,
                      const MimeType& mime_type_,
                      const Charset& charset_)
            : data(data_),
              mime_type(mime_type_),
              charset(charset_) {}

        /** Загруженные данные.
          * Может быть пустым, если тип mime_type не подлежал к загрузке.*/
        ::std::vector<char> data;

        /** Mime-тип загруженных данных. Заполнен всегда для http. */
        MimeType mime_type;

        /** Кодировка документа (исходя из http-сообщения сервера). */
        Charset charset;
    };


    /** Конструктор, создающий неиницилазированный объект.
      * Использовать только в качестве инициализатора стандартных контейнеров.
      * Для создания корректного объекта необходимо использовать метод
      * Curl::GetHandle(). */
    Handle()
        : handle_(NULL) {}


    /** Копирующий конструктор, инициализирующий объект низкоуровневым хэндлом
      * из rhv и увеличивающий счетчик ссылок, в случае если rhv был
      * инициализирован.
      * Таким образом, такую копию нельзя использовать в другом потоке. */
    Handle(const Handle& rhv)
        : handle_(rhv.handle_),
          http_header_extra_(rhv.http_header_extra_),
          ref_count_(rhv.ref_count_)
    {
        if ( handle_ != NULL )
            IncRefCount();
    }


    /** Оператор присваивания, инициализирующий объект низкоуровневым хэндлом
      * из rhv и увеличивающий счетчик ссылок, в случае если rhv был
      * инициализирован.
      * Таким образом, такую копию нельзя использовать в другом потоке. */
    Handle& operator=(const Handle& rhv)
    {
        if ( this != &rhv )
        {
            if ( this->handle_ != NULL && DecRefCount() == 0 )
                Destruct();

            this->handle_ = rhv.handle_;
            this->http_header_extra_ = rhv.http_header_extra_;
            this->ref_count_ = rhv.ref_count_;

            if ( this->handle_ != NULL )
                IncRefCount();
        }

        return *this;
    }


    ~Handle()
    {
        if ( handle_ != NULL && DecRefCount() == 0 )
            Destruct();
    }


    /** Клонирование хэндла. Данный метод создает новый объект со своим
      * счетчиком ссылок. Полученный хэндл можно использовать одновременно с
      * оригиналом в другом потоке. */
    Handle Clone() const
    {
        Handle clone;
        clone.handle_ = curl_easy_duphandle(handle_);
        clone.ref_count_ = new int(1);

        // стандартной функции дублирования нет. Но исходя из документации,
        // удалять список можно только после выполнения Perform.
        // Значит, он не копируется в низкоуровневый хэндл curl
        // и на каждый клон нужна своя полная копия.
        clone.http_header_extra_ = new_http_neader_extra();
        VERIFY(!curl_easy_setopt(clone.handle_, CURLOPT_HTTPHEADER,
                                 clone.http_header_extra_));
        return clone;
    }


    /** Задание адреса прокси в формате host[:port]. 
      * @pre объект должен быть инициализирован. */
    void set_proxy(const ::std::string& proxy)
    {
        PRECOND_MSG(handle_ != NULL, "Using default-constructed handle");
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_PROXY, proxy.c_str()));
    }


    /** Задание строки, передаваемой в качестве UserAgent.
      * Эта настройка позволяет "обмануть" удаленный сервер.
      * Проверка корректности производится только во время вызова Perform().
      * Повторный вызов перепишет предыдущее значение.
      * @pre объект должен быть инициализирован. */
    void set_user_agent(const ::std::string& user_agent)
    {
        PRECOND_MSG(handle_ != NULL, "Using default-constructed handle");
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_USERAGENT,
                                 user_agent.c_str()));
    }


    /** Задание строки, передаваемой в качестве Referer.
      * Эта настройка позволяет "обмануть" удаленный сервер.
      * Проверка корректности производится только во время вызова Perform().
      * Повторный вызов перепишет предыдущее значение.
      * @pre объект должен быть инициализирован. */
    void set_referer(const ::std::string& referer)
    {
        PRECOND_MSG(handle_ != NULL, "Using default-constructed handle");
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_REFERER, referer.c_str()));
    }


    /** Величина времени ожидания соединения в секундах.
      * Повторный вызов перепишет предыдущее значение.
      * @pre объект должен быть инициализирован. */
    void set_connect_timeout(const size_t timeout)
    {
        PRECOND_MSG(handle_ != NULL, "Using default-constructed handle");
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_CONNECTTIMEOUT,
                                 static_cast<long>(timeout)));
    }


    /** Величина максимального времени загрузки одной страницы в секундах.
      * Повторный вызов перепишет предыдущее значение.
      * @pre объект должен быть инициализирован. */
    void set_timeout(const size_t timeout)
    {
        PRECOND_MSG(handle_ != NULL, "Using default-constructed handle");
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_TIMEOUT,
                                 static_cast<long>(timeout)));
    }


    /** Задание минимальной скорости загрузки страницы.
      * Загрузка завершится в случае, если скорость соединения с ней
      * в течение duration секунд не превысит скорости speed б/с.
      * Проверка корректности производится только во время вызова Perform().
      * Повторный вызов перепишет предыдущее значение.
      * @pre объект должен быть инициализирован. */
    void set_low_speed_limit(const size_t speed, const size_t duration)
    {
        PRECOND_MSG(handle_ != NULL, "Using default-constructed handle");

        VERIFY(!curl_easy_setopt(handle_, CURLOPT_LOW_SPEED_LIMIT,
                                 static_cast<long>(speed)));

        VERIFY(!curl_easy_setopt(handle_, CURLOPT_LOW_SPEED_TIME,
                                 static_cast<long>(duration)));
    }


    /** Задание cookie-значений в формате "name1=var1;name2=var2;".
      * Повторный вызов перепишет предыдущее значение.
      * Проверка корректности производится только во время вызова Perform().
      * Повторный вызов перепишет предыдущее значение.
      * @pre объект должен быть инициализирован. */
    void set_cookie(const ::std::string& cookie)
    {
        PRECOND_MSG(handle_ != NULL, "Using default-constructed handle");
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_COOKIE, cookie.c_str()));
    }


    /** Задание максимального числа переходов (редиректов) при загрузке
      * одного документа. Значение 0 отключает переходы.
      * Повторный вызов перепишет предыдущее значение.
      * @pre объект должен быть инициализирован. */
    void set_max_num_redirects(const size_t num_redirects)
    {
        PRECOND_MSG(handle_ != NULL, "Using default-constructed handle");

        if ( num_redirects == 0 )
        {
            VERIFY(!curl_easy_setopt(handle_, CURLOPT_FOLLOWLOCATION, 0));
            VERIFY(!curl_easy_setopt(handle_, CURLOPT_MAXREDIRS, 0));
        }
        else
        {
            VERIFY(!curl_easy_setopt(handle_, CURLOPT_FOLLOWLOCATION, 1));
            VERIFY(!curl_easy_setopt(handle_, CURLOPT_MAXREDIRS,num_redirects));
            VERIFY(!curl_easy_setopt(handle_, CURLOPT_UNRESTRICTED_AUTH, 0));
            VERIFY(!curl_easy_setopt(handle_, CURLOPT_AUTOREFERER, 1));
        }
    }


    /** Очищение всех присланных cookie.
      * Метод не удаляет отправляемые cookie, заданные методом set_cookie.
      * Для удаления всех cookie следует воспользоваться парой методов:
      * clear_cookies(); set_cookie("");
      * @pre объект должен быть инициализирован. */
    void clear_cookies()
    {
        PRECOND_MSG(handle_ != NULL, "Using default-constructed handle");

        // Передается magic-строка.
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_COOKIELIST, "ALL"));
    }


    /** Выполнение загрузки документа по адресу url.
      * Все предшествующие вызовы set_X будут использованы в качестве опций.
      * Повторная загрузка с одного и того же сервера, выполняется быстрее,
      * так как используется одно и то же соединение.
      * @pre объект должен быть инициализирован.
      * @param url адрес, соответствующий RFC 2396.
      *        Пример для протокола file в Windows: file://c/file.txt.
      * @param file_type_filter функтор, определяющий по типу документа,
      *        следует ли продолжать загрузку или его нужно пропустить.
      *        Функтор должен быть задан со следующей сигнатурой:
      *        int operator()(const AsciiUrl& url,
      *                       const MimeType& mime_type,
      *                       const Charset& charset,
      *                       const ::std::vector<char>& data) const,
      *        не генерировать исключения, которые не сможет обработать C-код
      *        и выдавать один из 3 результатов:
      *          - TFileTypeFilter::CONTINUE_DOWNLOAD — тип документа
      *            определен, следует продолжить его загрузку
      *          - TFileTypeFilter::NEED_MORE_DATA — тип документа определить
      *            не удалось, необходимо считать заголовок документа, но
      *            переданный объем данных слишком мал (в таком случае будет
      *            загружена еще одна порция файла и функтор вызван повторно),
      *          - TFileTypeFilter::BREAK_DOWNLOAD — тип документа определен,
      *            следует прервать загрузку.
      *        Внимание: mime-тим состоит из двух слов, разделенных '/'.
      *        Проверка mime-типа должна не учитывать регистр.
      *        mime_type и charset могут быть пустыми, если сервер не передаст
      *        соответствующие сообщения в http-заголовке.
      * @note Если необходимо, метод совершит неограниченное число редиректов,
      *       если файл будет пустым, то и все возвращенные поля будут пустыми.
      * @return структура PerformResult.
      * @sa PerformResult.
      * @sa DefaultFileTypeFilter.
      * @exception FileSkippedException при возврате функтором FileTypeFilter
      *            значения TFileTypeFilter::BREAK_DOWNLOAD.
      *            CurlException в случае некорректных опций или
      *            при возникновении ошибки во время загрузки. */
    template<class TFileTypeFilter>
    PerformResult Perform(const AsciiUrl& url,
                          const TFileTypeFilter& file_type_filter)
    {
        PRECOND_MSG(handle_ != NULL, "Using default-constructed handle");

        // Буфер, используемый для сохранения текста ошибок.
        ::std::vector<char> error(CURL_ERROR_SIZE, 0);
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_ERRORBUFFER, &error[0]));

        // Функция, получающая mime-тип документа из заголовка протокола,
        // если он есть.
        ::std::string header;
        HeaderFunctionUserData hfud(&header);
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_HEADERDATA, &hfud));
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_HEADERFUNCTION,
                                 HeaderFunction));

        MimeType mime_type;
        Charset charset;
        ::std::vector<char> data;
        int write_function_result = WF_OK;
        WriteFunctionUserData<TFileTypeFilter> wfud(
            &file_type_filter, &url, &header,
            &mime_type, &charset, &data, &write_function_result);
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_WRITEDATA, &wfud));

        // Данным трюком мы обманываем MSVC 8 (возможно и более поздние версии),
        // который не хочет передавать указатель на шаблон функции через ...
        // (varargs). gcc этот код компилирует без предупреждений.
        typedef size_t (*WriteFunctionPtr)(void*, size_t, size_t, void*);
        WriteFunctionPtr wf = WriteFunction<TFileTypeFilter>;
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_WRITEFUNCTION, wf));


        VERIFY(!curl_easy_setopt(handle_, CURLOPT_URL, url.c_str()));

        const CURLcode curl_code = curl_easy_perform(handle_);

        // Переписываем обработчики, так как они нам больше не нужны,
        // а curl вызывает их в деструкторе, передавая ему прощальные сообщения
        // от FTP. Но так как к этому момент user_data указывает вникуда,
        // возникает ошибка памяти.
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_HEADERDATA, NULL));
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_WRITEDATA, NULL));
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_HEADERFUNCTION, NULL));
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_WRITEFUNCTION, NULL));


        // Ошибка CURL_WRITE_ERROR может возникнуть только в случае,
        // если WriteFunction вернул неправильный размер обработанных данных.
        // Но в реализации это зависит только от проверки типа файла фильтром.
        // Таким образом, такая ошибка — сигнал о прерванной фильтром загрузке.
        if ( curl_code != CURLE_OK )
        {
            if ( curl_code != CURLE_WRITE_ERROR )
                throw CurlException(&error[0]);

            if ( write_function_result == WF_HEADER_ERROR )
                throw CurlException("Header parse error");

            if ( write_function_result == WF_FILTER_ERROR )
                throw CurlException("Unknown filter result code");

            throw FileSkippedException();
        }

        return PerformResult(data, mime_type, charset);
    }


    /** Перегрузка метода Perform, загружающая все типы документов. */
    PerformResult Perform(const AsciiUrl& url)
    {
        return Perform(url, DefaultFileTypeFilter());
    }


    /** Метод возвращает хэндл библиотеки CURL, который можно использовать
      * для более точной настройки загрузки или после расширения API при
      * обновлении библиотеки. Запрещено применение вызова curl_easy_cleanup. */
    CURL* low_level_handle() const
    {
        return handle_;
    }


  private:
    /** WriteFunctionUserData::result_code. */
    enum { WF_OK, WF_HEADER_ERROR, WF_FILTER_ERROR, WF_BREAK_DOWNLOAD };


    /** Распознаватель типа файла, разрешающий загрузку любого документа. */
    class DefaultFileTypeFilter
    {
      public:
        // В не зависимости от того, что может вернуть operator() в области
        // видимости класса должны быть определены все 3 константы.
        // Значение констант — произвольно, результат сравнивается с именем.
        enum { CONTINUE_DOWNLOAD = 1, NEED_MORE_DATA = 0, BREAK_DOWNLOAD = -1 };

        /* Предикат всегда возвращает положительный результат. */
        int operator()(const AsciiUrl& /*url*/,
                       const MimeType& /*mime_type*/,
                       const Charset& /*charset*/,
                       const ::std::vector<char>& /*data*/) const
        {
            return CONTINUE_DOWNLOAD;
        }
    };


    /** Пользовательская структура, передаваемая curl, для использования в
      * функции обратного вызова WriteFunction. */
    template<class TFileTypeFilter>
    struct WriteFunctionUserData
    {
        WriteFunctionUserData(const TFileTypeFilter* file_type_filter_,
                              const AsciiUrl* url_,
                              const ::std::string* header_,
                              MimeType* mime_type_,
                              Charset* charset_,
                              ::std::vector<char>* data_,
                              int* result_code_)
            : call_file_type_filter(true),
              call_header_parser(true),
              file_type_filter(file_type_filter_),
              url(url_),
              header(header_),
              mime_type(mime_type_),
              charset(charset_),
              data(data_),
              result_code(result_code_) {}

        /** Флаг, определяющий нужно ли вызывать функтор file_type_filter_
          * при следующем вызове WriteFunction. */
        bool call_file_type_filter;

        /** Флаг, определяющий нужно ли вызывать парсер заголовка,
          * для получения mime-типа и кодировки. */
        bool call_header_parser;

        /** Функтор, определяющий нужно ли продолжать загрузку документа по
          * его mime-типу, url и загруженным данным. Таким образом, возможно
          * чтение заголовка файла.
          * @sa Handle::Perform. */
        const TFileTypeFilter* file_type_filter;

        /** URL загружаемого документа. */
        const AsciiUrl* url;

        /** Заголовок загружаемого документа. */
        const ::std::string* header;

        /** Буфер, в который будет записан mime-тип документа.
          * Может быть пустым, если протокол не поддерживает
          * передачу mime-типов (например, file:// или ftp://)
          * или в случае, если сервер сообщит его. */
        MimeType* mime_type;

        /** Буфер, в который будет записана кодировка документа.
          * Может быть пустым, если протокол не поддерживает
          * передачу mime-типов (например, file:// или ftp://)
          * или в случае, если сервер сообщит его. */
        Charset* charset;

        /** Буфер, в котором будут накапливаться загруженные данные. */
        ::std::vector<char>* data;

        /** Результат работы WriteFunction
          * (curl отличает только 2: CURLE_OK и CURLE_WRITE_ERROR). */
        int* result_code;
    };



    struct HeaderFunctionUserData
    {
        explicit HeaderFunctionUserData(::std::string* header_)
            : header(header_) {}

        /** Буфер, в который будут записаны полученные заголовки. */
        ::std::string* header;
    };



    /** Функция обратного вызова, используемая curl для передачи порций
      * скачанных данных пользователю. Перед началом сохранения данных
      * проверяется указатель на буфер, в который HeaderFunction записала
      * mime-тип документа и если он не является элементом
      * mime_types_to_download загрузка прерывается, а Perform обрабатывает
      * соответствующий код ошибки.
      * @param[in] pchar_data_block порция скачанных данных
      * @param[in] data_block_size размер (возможно, нулевой) порции данных
      *            в элементах element_size
      * @param[in] element_size размер элемента в байтах
      * @param[in] user_data заранее переданный (через
      *            curl_easy_setopt) указатель на буфер, который используется
      *            для накапливания данных.
      * @return объем обработанных данных в байтах. В случае, если не равен
      *         data_block_size * element_size загрузка документа завершается с
      *         соответствующей ошибкой. */
    template<class TFileTypeFilter>
    static size_t WriteFunction(void* pchar_data_block,
                                size_t data_block_size,
                                size_t element_size,
                                void* puser_data)
    {
        const size_t size = data_block_size * element_size;
        WriteFunctionUserData<TFileTypeFilter>* user_data =
            libv::PointerCast<WriteFunctionUserData<TFileTypeFilter> >(
                puser_data);
        const char* data_block = libv::PointerCast<char>(pchar_data_block);

        MSG("WriteFunction() callback is called, size = " << size );
        if ( user_data->call_header_parser )
        {
            MSG("    calling HttpHeaderParser");
            HttpHeaderParser parser(*user_data->header);
            const int r = parser.Parse();
            MSG("    HttpHeaderParser returned code " << r);


            if ( r == -1 )
            {
                *user_data->result_code = WF_HEADER_ERROR;
                user_data->mime_type->clear();
                user_data->charset->clear();
                return 0;
            }
            else if ( r == 1 )  // не http-заголовок.
            {
                user_data->mime_type->clear();
                user_data->charset->clear();
            }
            else
            {
                assert(r == 0);
                *user_data->mime_type = parser.mime_type();
                *user_data->charset = parser.charset();
            }
            user_data->call_header_parser = false;
        }

        // О реаллокации позаботится ::std::allocator
        user_data->data->insert(user_data->data->end(),
                                data_block, data_block + size);

        MSG("    calling file type filter");

        // Передаем все накопленные данные.
        // Если нужно оборвать закачку — очистим их.
        const int check_result = (*user_data->file_type_filter)(
            *user_data->url,
            *user_data->mime_type,
            *user_data->charset,
            *user_data->data);

        if ( check_result == TFileTypeFilter::BREAK_DOWNLOAD )
        {
            MSG("    file type filter returned code BREAK_DOWNLOAD");
            user_data->data->clear();
            *user_data->result_code = WF_BREAK_DOWNLOAD;
            return 0;
        }
        else if ( check_result == TFileTypeFilter::CONTINUE_DOWNLOAD )
        {
            MSG("    file type filter returned code CONTINUE_DOWNLOAD");
            user_data->call_file_type_filter = false;
        }
        else if ( check_result == TFileTypeFilter::NEED_MORE_DATA )
        {
            MSG("    file type filter returned code NEED_MORE_DATA");
            user_data->call_file_type_filter = true;
        }
        else
        {
            ERR("    file type filter returned unknown code");
            *user_data->result_code = WF_FILTER_ERROR;
            return 0;
        }

        return size;
    }


    /** Функция обратного вызова, используемая curl для передачи целых
      * необязательно NULL-терминированных строк http-заголовка.
      * Используется для сохранения всех сообщений в буфер.
      * @param[in] pchar_http_header_string строка http-заголовка
      * @param[in] data_block_size размер размер строки в элементах element_size
      * @param[in] element_size размер элемента в байтах
      * @param[in] used_data указатель на пользовательские данные типа
      *            HeaderFunctionUserData, используемая для ввода-вывода.
      * @return объем обработанных данных в байтах. В случае, если не равен
      *         data_block_size * element_size загрузка документа завершается с
      *         соответствующей ошибкой. */
    static size_t HeaderFunction(void* pchar_http_header_string,
                                 size_t data_block_size,
                                 size_t element_size,
                                 void* puser_data)
    {
        const size_t size = data_block_size * element_size;
        HeaderFunctionUserData* user_data =
            libv::PointerCast<HeaderFunctionUserData>(puser_data);

        MSG("HeaderFunction() callback is called, size = " << size );

        const char* http_message = libv::PointerCast<const char>(
            pchar_http_header_string);

        user_data->header->append(http_message, size);
        return size;
    }



    /** Создание инициализированного объекта.
      * Конструктор доступен классу Curl.
      * @exception libv::VerifyException в случае внутренней ошибки curl. */
    explicit Handle(CURLSH* curlsh)
      : handle_(NULL),
        http_header_extra_(NULL),
        ref_count_(new int(1))
    {
        // Все VERIFY, идущие ниже могут сгенерировать исключение, после чего
        // handle_ не будет корректно удален.
        // Однако VERIFY нужен только для отладки и проверки корректности
        // параметров и после его генерации приложение будет завершено.

        handle_ = curl_easy_init();
        VERIFY(handle_ != NULL);

        // По умолчанию отключена выдача отладочных сообщение в stderr.
		if ( g_verbose_level >= 3 )
	        VERIFY(!curl_easy_setopt(handle_, CURLOPT_VERBOSE, 1));
		else
	        VERIFY(!curl_easy_setopt(handle_, CURLOPT_VERBOSE, 0));


        // Выдача заголовков протокола (если поддерживаются) вместе с данными.
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_HEADER, 0));


        // Использование сигналов приводит к ошибкам в обработке таймаутов в
        // многопоточной среде.
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_NOSIGNAL, 1));

        // Всегда переходить по не более чем по 10 редиректам и
        // указывать Referer откуда был совершен переход.
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_FOLLOWLOCATION, 1));
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_MAXREDIRS, 10));
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_AUTOREFERER, 1));

        // Некоторые серверы не отдают страницы, если не указан UserAgent,
        // поэтому задаем значение по умолчанию, которое можно переписать
        // через set_user_agent.
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_USERAGENT, "libcurl"));

        http_header_extra_ = new_http_neader_extra();
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_HTTPHEADER,
                                 http_header_extra_));

        // Активация cookie engine библиотеки curl.
        // После данного вызова присланные cookie будут сохраняться.
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_COOKIELIST, "Set-Cookie:"));

        // Запрет верификации безопасных соединений.
        // см. http://curl.haxx.se/docs/sslcerts.html
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYPEER, FALSE));

        // Все хэндлы имеют общий shared-handle, в котором они обмениваются
        // dns-resolve информацией.
        // Эти обращения защищены callback-блокировками.
        // Можно также иметь общие cookie, но незачем.
        VERIFY(!curl_easy_setopt(handle_, CURLOPT_SHARE, curlsh));
    }


    /** Уменьшение счетчика ссылок на 1. Возвращается новое значение. */
    int DecRefCount() const
    {
        PRECOND(handle_ != NULL && ref_count_ != NULL);
        return --(*ref_count_);
    }


    /** Увеличение счетчика ссылок на 1. Возвращается новое значение. */
    int IncRefCount() const
    {
        PRECOND(handle_ != NULL && ref_count_ != NULL);
        return ++(*ref_count_);
    }


    /** Уничтожение объекта (деструктор).
      * Метод вызывается, когда уничтожается последняя ссылка на объект. */
    void Destruct() const
    {
        PRECOND(handle_ != NULL && ref_count_ != NULL);
        assert(*ref_count_ == 0);

        delete ref_count_;

        curl_slist_free_all(http_header_extra_);

        curl_easy_cleanup(handle_);
    }


    /** Создание списка с дополнительными сообщениями для http-заголовка.
      * Список должен быть удален через метод curl_slist_free_all при удалении
      * низкоуровневого хэндла. */
    curl_slist* new_http_neader_extra() const
    {
        // Сurl поддерживает keep-alive, однако не сообщает об этом удаленному
        // серверу, так как в HTTP 1.1 этот режим поддерживается по умолчанию.
        // Однако, без этой дополнительной строки некоторые сайты не
        // отдают информацию (пример: zalil.ru/..., отдается http-заголовок,
        // но не данные). Возможно, таким образом, отсекаются небраузеры,
        // firefox высылает такое сообщение. Таким образом, это "подстраховка".
        // См. http://www.io.com/~maus/HttpKeepAlive.html

        curl_slist* result = NULL;
        result = curl_slist_append(result, "Connection: keep-alive");
        VERIFY(result != NULL);
        return result;
    }


    /** Низкоуровневый curl-хэндл, которым владеет данный объект. */
    CURL* handle_;

    /** Дополнительные строки, передаваемые в http заголовке. */
    curl_slist* http_header_extra_;

    /** Счетчик ссылок на данных объект. */
    mutable int* ref_count_;
};



// Метод использует тип Curl::Handle, который объявлен ниже типа Curl.
Curl::Handle Curl::GetHandle() const
{
    libv::Lock dnd(&lock_);

    Handle result(curlsh_);

    if ( default_proxy_is_set_ )
        result.set_proxy(default_proxy_);

    if ( default_user_agent_is_set_ )
        result.set_user_agent(default_user_agent_);

    if ( default_referer_is_set_ )
        result.set_referer(default_referer_);

    if ( default_connect_timeout_is_set_ )
        result.set_connect_timeout(default_connect_timeout_);

    if ( default_timeout_is_set_ )
        result.set_timeout(default_timeout_);

    if ( default_low_speed_limit_is_set_ )
        result.set_low_speed_limit(default_low_speed_limit_speed_,
                                   default_low_speed_limit_duration_);

    if ( default_cookie_is_set_ )
        result.set_cookie(default_cookie_);

    if ( default_max_num_redirects_is_set_ )
        result.set_max_num_redirects(default_max_num_redirects_);

    return result;
}

}  // scrw

#endif  // SCRW_THE_CURL_H_
